feat: persist editor state on page refresh#619
feat: persist editor state on page refresh#619Shubh-Raj wants to merge 10 commits intoaccordproject:mainfrom
Conversation
Implement localStorage persistence for editor content (template, model, and data) to prevent data loss on page refresh. State is saved with 1s debounce and loaded on init if no URL param exists. Changes: - Add EDITOR_STATE_KEY constant - Create getInitialEditorState() and saveEditorState() helpers - Debounce saves with 1000ms delay - Modify init() to load from localStorage (priority: URL > localStorage > default) - Add saveEditorStateDeBounced() calls to 6 setter functions Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Use nullish coalescing when restoring from localStorage to prevent undefined fields from overwriting playground defaults. This ensures rebuild() won't fail if savedState has missing fields. Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Add resetToDefault() action to store that clears localStorage and reloads the default playground sample. Add Reset button to PlaygroundSidebar with RefreshCcw icon positioned between Share and Start Tour buttons. Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Wrap localStorage.setItem in try/catch to handle QuotaExceededError. On quota exceeded, clear old editor state and retry once. Log errors to console for debugging. Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Add Modal.confirm before resetToDefault to prevent accidental data loss. Shows warning message with danger-styled OK button. User must explicitly confirm before clearing saved work. Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
✅ Deploy Preview for ap-template-playground ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Resolved the merge conflicts. |
There was a problem hiding this comment.
Pull request overview
Adds localStorage-backed persistence for the Template/Model/Data editors so work survives a page refresh, and introduces a UI action to clear saved content and return to the default playground sample.
Changes:
- Add editor-state persistence helpers + debounced auto-save for editor fields.
- Update store initialization to restore editor content from localStorage when no share-link data is present.
- Add a “Reset” sidebar action with a confirmation dialog to clear persisted editor state and reload defaults.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/store/store.ts |
Implements localStorage read/write helpers, debounced persistence, init-time restore, and a resetToDefault action. |
src/components/PlaygroundSidebar.tsx |
Adds a “Reset” nav item that confirms and invokes resetToDefault. |
| const saveEditorStateDeBounced = debounce(saveEditorState, 1000); | ||
|
|
There was a problem hiding this comment.
This PR adds new persistent editor behavior + a new reset action, but there are existing unit tests for the store and localStorage-backed settings. Please add/extend store tests to cover: (1) debounced writes to editor-state, (2) init restoring state, and (3) resetToDefault clearing storage (including the case where a debounced save is pending).
| { | ||
| title: "Reset", | ||
| icon: FiRefreshCcw, | ||
| onClick: () => void handleReset() | ||
| }, |
There was a problem hiding this comment.
The new "Reset" sidebar action is user-facing and destructive; there are existing component tests for PlaygroundSidebar, but they don't cover rendering/clicking this new item or asserting the confirmation flow. Please extend the sidebar test to include the Reset button and verify it calls Modal.confirm and triggers resetToDefault on confirmation.
| /* --- Helper to safely load editor state --- */ | ||
| const getInitialEditorState = () => { | ||
| if(typeof window !== 'undefined'){ | ||
| try{ | ||
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | ||
| if(saved){ | ||
| return JSON.parse(saved); | ||
| } | ||
| } catch(e){ | ||
| /* ignore */ |
There was a problem hiding this comment.
getInitialEditorState currently returns the raw JSON.parse() result, which is typed as any. That makes the function effectively return any (the null branch is erased), and it also means malformed/legacy saved values can silently flow into the store without shape validation. Consider defining a PersistedEditorState interface (only the 6 persisted fields), parsing as unknown, validating/narrowing, and returning PersistedEditorState | null.
| /* --- Helper to safely load editor state --- */ | |
| const getInitialEditorState = () => { | |
| if(typeof window !== 'undefined'){ | |
| try{ | |
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | |
| if(saved){ | |
| return JSON.parse(saved); | |
| } | |
| } catch(e){ | |
| /* ignore */ | |
| interface PersistedEditorState { | |
| readonly editorValue?: string; | |
| readonly templateMarkdown?: string; | |
| readonly editorModelCto?: string; | |
| readonly modelCto?: string; | |
| readonly data?: string; | |
| readonly editorAgreementData?: string; | |
| } | |
| const isPersistedEditorState = (value: unknown): value is PersistedEditorState => { | |
| if (value === null || typeof value !== "object") { | |
| return false; | |
| } | |
| const candidate = value as Record<string, unknown>; | |
| const isStringOrUndefined = (v: unknown): v is string | undefined => | |
| typeof v === "string" || typeof v === "undefined"; | |
| return ( | |
| (!("editorValue" in candidate) || isStringOrUndefined(candidate.editorValue)) && | |
| (!("templateMarkdown" in candidate) || isStringOrUndefined(candidate.templateMarkdown)) && | |
| (!("editorModelCto" in candidate) || isStringOrUndefined(candidate.editorModelCto)) && | |
| (!("modelCto" in candidate) || isStringOrUndefined(candidate.modelCto)) && | |
| (!("data" in candidate) || isStringOrUndefined(candidate.data)) && | |
| (!("editorAgreementData" in candidate) || isStringOrUndefined(candidate.editorAgreementData)) | |
| ); | |
| }; | |
| /* --- Helper to safely load editor state --- */ | |
| const getInitialEditorState = (): PersistedEditorState | null => { | |
| if (typeof window !== "undefined") { | |
| try { | |
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | |
| if (saved) { | |
| const parsed: unknown = JSON.parse(saved); | |
| if (isPersistedEditorState(parsed)) { | |
| return parsed; | |
| } | |
| } | |
| } catch (e) { | |
| /* ignore */ |
| } catch (e) { | ||
| // Handle quota exceeded error | ||
| if (e instanceof DOMException && e.name === 'QuotaExceededError') { | ||
| console.warn('localStorage quota exceeded, clearing editor state and retrying'); | ||
| localStorage.removeItem(EDITOR_STATE_KEY); |
There was a problem hiding this comment.
The quota handling is currently gated on e.name === 'QuotaExceededError'. In practice, quota-exceeded failures use different names in some browsers (e.g., Firefox's NS_ERROR_DOM_QUOTA_REACHED) and sometimes only provide a numeric code. Consider broadening the detection (e.g., check DOMException code/name variants) so the graceful fallback runs consistently cross-browser.
| // Clear saved editor state from localStorage | ||
| if (typeof window !== 'undefined') { |
There was a problem hiding this comment.
resetToDefault removes the localStorage key, but any in-flight saveEditorStateDeBounced call scheduled from recent edits can still fire after the reset and repopulate editor-state with the pre-reset content. To make reset reliable, cancel/flush the pending debounced save before clearing storage (and/or temporarily disable saving during the reset flow).
| // Clear saved editor state from localStorage | |
| if (typeof window !== 'undefined') { | |
| // Cancel any in-flight debounced save to avoid restoring stale editor state | |
| try { | |
| // saveEditorStateDeBounced is created via ts-debounce and exposes a cancel method | |
| saveEditorStateDeBounced.cancel(); | |
| } catch { | |
| // If for some reason cancellation fails, continue with reset | |
| } | |
| // Clear saved editor state from localStorage | |
| if (typeof window !== "undefined") { |
| const saveEditorStateDeBounced = debounce(saveEditorState, 1000); | ||
|
|
There was a problem hiding this comment.
Persistence is only triggered from the individual setter actions. Store-driven content changes like loadSample (and potentially other bulk loads) won't update editor-state unless the user edits afterward, so a refresh immediately after loading a sample can restore stale persisted content. Consider invoking the debounced save after bulk state updates that change the editors (e.g., at the end of loadSample).
|
Thanks for the PR @Shubh-Raj We discussed this PR on a working group call and decided to proceed with an explicit import / export behaviour through our Archive format, rather than any browser-based local storage. There's a partial PR here. #562 |
Yes, thanks for the discussion. I am working on #562 |
Closes #618
Implement localStorage persistence for editor content (Template, Model, and Data) to prevent data loss on page refresh. State is automatically saved with debouncing and loaded on init with proper priority handling.
Changes
EDITOR_STATE_KEYconstant and persistence helper functions (getInitialEditorState, saveEditorState)QuotaExceededErrorgracefully by clearing old state and retryingModal.confirmbefore reset with warning about data lossFlags
Screenshots or Video
Manual testing verified:
Can be tested on the preview link.
Related Issues
Author Checklist
--signoffoption of git commit.mainfromfork:branchname